THREE.SWCLoader = function(manager) {
	manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;
};

THREE.SWCLoader.prototype = {
	constructor: THREE.SWCLoader,
	//Load a neuron from swc file
	//Inputs:
	//   url - path to swc file
	//   mode - 'skeleton', 'sphere', or 'particle' mode for rendering
	//   fov - feild of view for camera to render neuron (needed for particle mode)
	//   canvas_height - height of canvas (needed for particle mode)
	//   device_pixel_ratio - from three.js renderer: renderer.devicePixelRatio 
	//   load_fcn - callback function when swc loads, should accept 1 parameter, the neuron to render
	load: function(url, mode, fov, canvas_height, device_pixel_ratio, load_fcn) {
		var scope = this;
		var loader = new THREE.XHRLoader( scope.manager );
		loader.setCrossOrigin( scope.crossOrigin );
		loader.load( url, function ( text ) {
			var json = scope.parseSWC( text );
			scope.createModel( json, mode, fov, canvas_height, device_pixel_ratio, load_fcn );
		} );
	},

	parseSWC: function(swc_file) {
		//split by lines
		var swc_ar = swc_file.split("\n");
		var swc_json = {};

		var float = '-?\\d*(?:\\.\\d+)?';
		var pattern = new RegExp('^[ \\t]*(' + [
			'\\d+',   // index
			'\\d+',  // type
			float,    // x
			float,    // y
			float,    // z
			float,    // radius
			'-1|\\d+' // parent
		].join(')[ \\t]+(') + ')[ \\t]*$');

		swc_ar.forEach(function (e) {
			//if line is good, put into json
			var match = e.match(pattern);
			if (match) {
				swc_json[match[1]] = {
					'type'   : parseInt  (match[2]),
					'x'      : parseFloat(match[3]),
					'y'      : parseFloat(match[4]),
					'z'      : parseFloat(match[5]),
					'radius' : parseFloat(match[6]),
					'parent' : parseInt  (match[7]),
				};
			}
		});

		//return json
		return swc_json;
	},
	
	createModel: function(json, mode, fov, canvas_height, device_pixel_ratio, callback) {
		var swc = json; 
		//which node to center neuron on (starts at 1), -1 to center at center of bounding box
		var center_node = 1; 
		//show cones between cylindars for particle and sphere mode
		var show_cones = true;
		//color array, nodes of type 0 show as first color, etc. 
		var colors = [
			0x00be9e,
			0x3919cb,
			0x7d0bc4,
			0xff6700,
			0x3eef00,
			0xffce00,
			0xf50027,
			0x606060,
		]; 
		var boundingBox, three_materials, nodeColor, three_colors, center, geometry, neuron, material;
		var n = init();
		function calculateBoundingBox() {	
			for (var node in swc) {
				if (swc.hasOwnProperty(node)) {
					if (swc[node].x < boundingBox.xmin) boundingBox.xmin = swc[node].x;
					if (swc[node].x > boundingBox.xmax) boundingBox.xmax = swc[node].x;
					if (swc[node].y < boundingBox.ymin) boundingBox.ymin = swc[node].y;
					if (swc[node].y > boundingBox.ymax) boundingBox.ymax = swc[node].y;
					if (swc[node].z < boundingBox.zmin) boundingBox.zmin = swc[node].z;
					if (swc[node].z > boundingBox.zmax) boundingBox.zmax = swc[node].z;
				}
			}
		};
		function generateSphere(node) {
			var sphereMaterial = three_materials[ node.type ];
			var r1 = node.radius || 0.01;
			var geometry = new THREE.SphereGeometry(r1);
			var mesh = new THREE.Mesh(geometry, sphereMaterial);
			mesh.position.x = node.x;
			mesh.position.y = node.y;
			mesh.position.z = node.z;
			return mesh;
		};
		function generateAnnotationSphere(annotation, node) {
			var black = new THREE.Color(0x000000);
			var sphereMaterial = new THREE.MeshBasicMaterial({ color: black});
			var r1 = (node.radius * 0.25) || 0.01;
			var geometry = new THREE.SphereGeometry(r1);
			var mesh = new THREE.Mesh(geometry, sphereMaterial);
			mesh.position.x = annotation.x;
			mesh.position.y = annotation.y;
			mesh.position.z = annotation.z;
			return mesh;
		};

		//generates cones connecting spheres
		function generateConeGeometry(node, node_parent) {
			var coneMaterial = three_materials[ node_parent.type ];
			var node_vec = new THREE.Vector3(node.x, node.y, node.z); 
			var node_parent_vec = new THREE.Vector3(node_parent.x, node_parent.y, node_parent.z); 
			var dist = node_vec.distanceTo(node_parent_vec);
			var cylAxis = new THREE.Vector3().subVectors(node_vec, node_parent_vec);
			cylAxis.normalize();
			var theta = Math.acos( cylAxis.y );
			var rotationAxis = new THREE.Vector3();
			rotationAxis.crossVectors(cylAxis, new THREE.Vector3(0,1,0));
			rotationAxis.normalize();
			var r1 = node.radius || 0.01;
			var r2 = node_parent.radius || 0.01;
			var geometry = new THREE.CylinderGeometry(r1, r2, dist);
			var mesh = new THREE.Mesh(geometry, coneMaterial);
			mesh.matrixAutoUpdate = false;
			mesh.matrix.makeRotationAxis( rotationAxis, -theta );	
			var position = new THREE.Vector3((node.x + node_parent.x) / 2, (node.y + node_parent.y) / 2,(node.z + node_parent.z) / 2);
			mesh.matrix.setPosition(position);
			return mesh;
		}; 
				
		//generates particle verticies
		function generateParticle(node) {
			return new THREE.Vector3(node.x, node.y, node.z);
		};


		//generates skeleton verticies
		function generateSkeleton(node, node_parent) {
			var vertex = new THREE.Vector3(node.x, node.y, node.z);
			var vertex_parent = new THREE.Vector3(node_parent.x, node_parent.y, node_parent.z);
			var child_color = nodeColor(node);
			var parent_color = nodeColor(node_parent);
			return {
				'child' : vertex, 
				'parent' : vertex_parent,
				'child_color' : child_color,
				'parent_color' : parent_color
			};
		};

		//generates cone properties for node, parent pair
		function generateCone(node, node_parent) {
			var cone_child = {};
			var cone_parent = {};
			
			cone_child.vertex = new THREE.Vector3(node.x, node.y, node.z);
			cone_child.radius = node.radius;
			cone_child.color = nodeColor(node);
			
			cone_parent.vertex = new THREE.Vector3(node_parent.x, node_parent.y, node_parent.z);
			cone_parent.radius = node_parent.radius;
			cone_parent.color = nodeColor(node_parent);

			//normals
			var n1 = new THREE.Vector3().subVectors(cone_parent.vertex, cone_child.vertex);
			var n2 = n1.clone().negate();
				
			return {
				'child' : cone_child, 
				'parent' : cone_parent, 
				'normal1' : n1, 
				'normal2': n2, 
			};
		};
			
		//calculates color based on node type
		function nodeColor(node) {
			return three_colors[ node.type % three_colors.length];
		};
		//Sets up three.js scene 
		function init() {

			//set up shaders
			var vertexShader =  [
				'uniform float particleScale;',
				'attribute float radius;',
				'attribute vec3 typeColor;',
				'varying vec3 vColor;',
				'varying vec4 mvPosition;',
				'varying float vRadius;',
				'void main() ',
				'{',
					'vColor = vec3(typeColor); // set RGB color associated to vertex; use later in fragment shader.',
					'mvPosition = modelViewMatrix * vec4(position, 1.0);',

					'// gl_PointSize = size;',
					'gl_PointSize = radius * ((particleScale*2.0) / length(mvPosition.z));',
					'gl_Position = projectionMatrix * mvPosition;',
					'vRadius = radius;',
				'}'
			].join("\n");
			
			var fragmentShader = [
				'#extension GL_EXT_frag_depth : enable',
				'uniform sampler2D sphereTexture; // Imposter image of sphere',
				'uniform mat4 projectionMatrix;',
				'varying vec3 vColor; // colors associated to vertices; assigned by vertex shader',
				'varying vec4 mvPosition;',
				'varying float vRadius;',
				'void main() ',
				'{',
					'// what part of the sphere image?',
					'vec2 uv = vec2(gl_PointCoord.x, 1.0 - gl_PointCoord.y);',
					'vec4 sphereColors = texture2D(sphereTexture, uv);',
						'// avoid further computation at invisible corners',
					'if (sphereColors.a < 0.3) discard;',

					'// calculates a color for the particle',
					'// gl_FragColor = vec4(vColor, 1.0);',
					'// sets a white particle texture to desired color',
					'// gl_FragColor = sqrt(gl_FragColor * texture2D(sphereTexture, uv)) + vec4(0.1, 0.1, 0.1, 0.0);',
					'// red channel contains colorizable sphere image',
					'vec3 baseColor = vColor * sphereColors.r;',
						'// green channel contains (white?) specular highlight',
					'vec3 highlightColor = baseColor + sphereColors.ggg;',
					'gl_FragColor = vec4(highlightColor, sphereColors.a);',
					'// TODO blue channel contains depth offset, but we cannot use gl_FragDepth in webgl?',
				'#ifdef GL_EXT_frag_depth',
					'float far = gl_DepthRange.far; float near = gl_DepthRange.near;', 
					'float dz = sphereColors.b * vRadius;', 
					'vec4 clipPos = projectionMatrix * (mvPosition + vec4(0, 0, dz, 0));', 
					'float ndc_depth = clipPos.z/clipPos.w;', 
					'float depth = (((far-near) * ndc_depth) + near + far) / 2.0;', 
					'gl_FragDepthEXT = depth;',
				'#endif',
				'}'
			].join("\n");
			 
			var vertexShaderCone = [
				'attribute float radius;',
				'attribute vec3 typeColor;',
				'varying vec3 vColor;',
				'varying vec2 sphereUv;',
				'varying vec4 mvPosition;',
				'varying float depthScale;',
				'void main() ',
				'{',
				'	// TODO - offset cone position for different sphere sizes',
				'	// TODO - implement depth buffer on Chrome',
				'	mvPosition = modelViewMatrix * vec4(position, 1.0);',
				'	// Expand quadrilateral perpendicular to both view/screen direction and cone axis',
				'	vec3 cylAxis = (modelViewMatrix * vec4(normal, 0.0)).xyz; // convert cone axis to camera space',
				'	vec3 sideDir = normalize(cross(vec3(0.0,0.0,-1.0), cylAxis));',
				'	mvPosition += vec4(radius * sideDir, 0.0);',
				'	gl_Position = projectionMatrix * mvPosition;',
				'	// Pass and interpolate color',
				'	vColor = typeColor;',
				'	// Texture coordinates',
				'	sphereUv = uv - vec2(0.5, 0.5); // map from [0,1] range to [-.5,.5], before rotation',
				'	// If sideDir is "up" on screen, make sure u is positive',
				'	float q = sideDir.y * sphereUv.y;',
				'	sphereUv.y = sign(q) * sphereUv.y;',
				'	// rotate texture coordinates to match cone orientation about z',
				'	float angle = atan(sideDir.x/sideDir.y);',
				'	float c = cos(angle);',
				'	float s = sin(angle);',
				'	mat2 rotMat = mat2(',
				'		c, -s, ',
				'		s,  c);',
				'	sphereUv = rotMat * sphereUv;',
				'	sphereUv += vec2(0.5, 0.5); // map back from [-.5,.5] => [0,1]',
				'	// We are painting an angled cone onto a flat quad, so depth correction is complicated',
				'   float foreshortening = length(cylAxis) / length(cylAxis.xy); // correct depth for foreshortening',
				'   // foreshortening limit is a tradeoff between overextruded cone artifacts, and depth artifacts',
				'   if (foreshortening > 4.0) foreshortening = 0.9; // hack to not pop out at extreme angles...',
				'   depthScale = radius * foreshortening; // correct depth for foreshortening',
				'}',
			].join("\n");
			 
			var fragmentShaderCone = [
				'#extension GL_EXT_frag_depth : enable',
				'uniform sampler2D sphereTexture; // Imposter image of sphere',
				'uniform mat4 projectionMatrix;',
				'varying vec3 vColor;',
				'varying vec2 sphereUv;',
				'varying vec4 mvPosition;',
				'varying float depthScale;',
				'void main() ',
				'{',
				'	vec4 sphereColors = texture2D(sphereTexture, sphereUv);',
				'	if (sphereColors.a < 0.3) discard;',
				'	vec3 baseColor = vColor * sphereColors.r;',
				'	vec3 highlightColor = baseColor + sphereColors.ggg;',
				'	gl_FragColor = vec4(highlightColor, sphereColors.a);',
				'#ifdef GL_EXT_frag_depth',
					'float dz = sphereColors.b * depthScale;', 
					'vec4 mvp = mvPosition + vec4(0, 0, dz, 0);', 
					'vec4 clipPos = projectionMatrix * mvp;', 
					'float ndc_depth = clipPos.z/clipPos.w;', 
					'float far = gl_DepthRange.far; float near = gl_DepthRange.near;', 
					'float depth = (((far-near) * ndc_depth) + near + far) / 2.0;', 
					'gl_FragDepthEXT = depth;',
				'#endif',
				'}',
			].join("\n");

			
			 
			//set up colors and materials based on color array
			three_colors = [];
			for (var color in colors) {
				if (colors.hasOwnProperty(color)) {
					three_colors.push(new THREE.Color(colors[color]));
				}
			}
			three_materials = [];
			for (var color in colors) {
				if (colors.hasOwnProperty(color)) {
					three_materials.push(new THREE.MeshBasicMaterial({ color: colors[color] }));
				}
			}
				
			//initialize bounding box
			boundingBox = {
				'xmin' : swc['1'].x,
				'xmax' : swc['1'].x,
				'ymin' : swc['1'].y,
				'ymax' : swc['1'].y,
				'zmin' : swc['1'].z,
				'zmax' : swc['1'].z,
			};
			calculateBoundingBox();

			//neuron centers around 1st node by default
			if (center_node === -1) {
				center = [ (boundingBox.xmax + boundingBox.xmin) / 2, (boundingBox.ymax + boundingBox.ymin) / 2, (boundingBox.zmax + boundingBox.zmin) / 2  ] ;
			}
			else{
				center_node = center_node.toString();
				center = [ swc[center_node].x, swc[center_node].y, swc[center_node].z ];
			}
			
			//neuron is object 3d which ensures all components move together
			neuron = new THREE.Object3D();
			
			//particle mode uses vertex info to place texture image, very fast
			if (mode === 'particle') {

				//material = new THREE.ParticleSystemMaterial({color: 0x0080ff});
				// special imposter image contains:
				// 1 - colorizable sphere image in red channel
				// 2 - specular highlight in green channel
				// 3 - depth offset in blue channel (currently unused)
				var image = document.createElement( 'img' );
				var sphereImg = new THREE.Texture( image );
				image.onload = function()  {
					sphereImg.needsUpdate = true;
				};
				image.src = '';

				geometry = new THREE.Geometry();
				// properties that may vary from particle to particle. only accessible in vertex shaders!
				//	(can pass color info to fragment shader via vColor.)
				// compute scale for particles, in pixels
				var particleScale =  (0.5 * canvas_height * device_pixel_ratio) / Math.tan(0.5 * fov * Math.PI / 180.0);
			
				var customAttributes = 
				{
					radius:   { type: "fv1", value: [] },
					typeColor:   { type: "c", value: [] },
				};
				var customUniforms = 
				{
					particleScale: { type: 'f', value: particleScale },
					sphereTexture: { type: 't', value: sphereImg },
				};

				for (var node in swc) {
					if (swc.hasOwnProperty(node)) {
						var particle_vertex = generateParticle(swc[node]);
						geometry.vertices.push(particle_vertex);
						customAttributes.radius.value.push(swc[node].radius);
						customAttributes.typeColor.value.push(nodeColor(swc[node]));
					}
				} 

				material = new THREE.ShaderMaterial(
				{
					uniforms : customUniforms,
					attributes : customAttributes,
					vertexShader : vertexShader,
					fragmentShader : fragmentShader,
					transparent : true, 
					// alphaTest: 0.5,  // if having transparency issues, try including: alphaTest: 0.5, 
					depthTest : true,
					// blending: THREE.AdditiveBlending, depthTest: false,
					// I guess you don't need to do a depth test if you are alpha blending?
					// 
				});


				var particles = new THREE.ParticleSystem(geometry, material);
				particles.sortParticles = false;
				neuron.add(particles);

				if (show_cones){	
					// Cone quad imposters, to link spheres together
					var coneAttributes = 
					{
						radius:   { type: "fv1", value: [] },
						typeColor:   { type: "c", value: [] },
					};
					var coneUniforms = 
					{
						sphereTexture: { type: 't', value: sphereImg },
					};
					var uvs = [
						new THREE.Vector2(0.5, 0),
						new THREE.Vector2(0.5, 1),
						new THREE.Vector2(0.5, 1)
					];
					var coneGeom = new THREE.Geometry();
					for (var node in swc) {
						if (swc.hasOwnProperty(node)) {
							if (swc[node].parent != -1) {
								// Child/first position
								var cone = generateCone(swc[node], swc[swc[node].parent]);
								var ix2 = coneGeom.vertices.push(cone.child.vertex);
								coneAttributes.radius.value.push(cone.child.radius);
								coneAttributes.typeColor.value.push(cone.child.color);
								
								coneGeom.vertices.push(cone.parent.vertex);
								coneAttributes.radius.value.push(cone.parent.radius);
								coneAttributes.typeColor.value.push(cone.parent.color);
								
								// Paint two triangles to make a cone-imposter quadrilateral
								// Triangle #1
								var coneFace = new THREE.Face3(ix2 - 1, ix2 - 1, ix2);
								coneFace.vertexNormals = [ cone.normal1, cone.normal2, cone.normal2 ];
								coneGeom.faces.push(coneFace);
								// Simple texture coordinates should be modified in the vertex shader
								coneGeom.faceVertexUvs[0].push(uvs);
								// Triangle #2
								coneFace = new THREE.Face3(ix2, ix2, ix2-1);
								coneFace.vertexNormals = [ cone.normal1, cone.normal2, cone.normal1 ];
								coneGeom.faces.push(coneFace);
								coneGeom.faceVertexUvs[0].push(uvs);
							}
						}
					} 
					var coneMaterial = new THREE.ShaderMaterial(
					{
						attributes: coneAttributes,
						uniforms: coneUniforms,
						vertexShader:   vertexShaderCone,
						fragmentShader: fragmentShaderCone,
						transparent: false, 
						depthTest: true,
						side: THREE.DoubleSide
					});
					var coneMesh = new THREE.Mesh(coneGeom, coneMaterial);
					neuron.add(coneMesh);
				}
			}
			//sphere mode renders 3d sphere
			else if (mode === 'sphere') {
				for (var node in swc) {
					if (swc.hasOwnProperty(node)) {
						var sphere = generateSphere(swc[node]);
						neuron.add(sphere);
						if (show_cones){
							if (swc[node].parent != -1) {
								var cone = generateConeGeometry(swc[node], swc[swc[node].parent]);
								neuron.add(cone);
							}
						}
					}
				}
			}
			
			if (mode === 'skeleton' || show_cones === false) {
				material = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors  });
				geometry = new THREE.Geometry();
				for (var node in swc) {
					if (swc.hasOwnProperty(node)) {
						if (swc[node].parent != -1) {
							var verticies = generateSkeleton(swc[node], swc[swc[node].parent]);
							geometry.vertices.push(verticies.child);
							geometry.colors.push(verticies.child_color);
							geometry.vertices.push(verticies.parent);
							geometry.colors.push(verticies.parent_color);
						}
					}
				} 
				var line = new THREE.Line(geometry, material, THREE.LinePieces);
				neuron.add(line);
			}
			
			//centers neuron
			neuron.position.set(-center[0], -center[1], -center[2]);
			//callback function to use neuron
			callback(neuron);
		};
	}
}
